/**
 * @file   Client.cpp
 * @author Djuro Drljaca (djurodrljaca@gmail.com)
 * @date   2013-03-11
 * @brief  DataBus Client.
 *
 * Copyright (C) 2013  Djuro Drljaca <djurodrljaca@gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <DataBus/Server/Client.h>
#include <DataBus/Common/PingRequestPacket.h>

DataBus::Client::Client(QObject *parent)
    : QObject(parent),
      m_socket(0),
      m_clientId(0),
      m_activityTimeout(0),
      m_pingResponseTimeout(0),
      m_pingRetryCount(0),
      m_packetParser(),
      m_packetList(),
      m_packetIdCounter(0),
      m_timer(0),
      m_status(Client::Status_Unregisterd),
      m_activityTimeoutTimer(),
      m_pingResponseTimeoutTimer(),
      m_pingRetryCountCounter(0),
      m_lastPingRequestPacketId(0)
{
}

DataBus::Client::~Client()
{
    // Close and delete the TCP socket object
    close();
}

bool DataBus::Client::initialize(QTcpSocket *socket)
{
    // First close the clinet
    close();

    // Check socket
    if (socket == 0)
    {
        // Error, null pointer
        return false;
    }

    if (socket->state() != QTcpSocket::ConnectedState)
    {
        // Error, DataBus Client is no longer connected to this socket
        return false;
    }

    // Initialize object
    m_socket = socket;
    m_clientId = 0;
    m_activityTimeout = 0;
    m_pingResponseTimeout = 0;
    m_pingRetryCount = 0;
    m_packetParser.initialize();
    m_packetList.clear();
    m_packetIdCounter = 0;
    m_timer = new QTimer(this);
    m_status = Client::Status_Unregisterd;
    m_activityTimeoutTimer.start();
    m_pingResponseTimeoutTimer.start();
    m_pingRetryCountCounter = 0;
    m_lastPingRequestPacketId = 0;

    // Connect signals
    connect(m_socket, SIGNAL(disconnected()), this, SLOT(disconnected()));
    connect(m_socket, SIGNAL(readyRead()), this, SLOT(readData()));
    connect(m_timer, SIGNAL(timeout()), this, SLOT(processActivity()));

    // OK
    return true;
}

void DataBus::Client::close()
{
    // Close and delete the TCP socket
    if (m_socket != 0)
    {
        if (m_socket->isOpen())
        {
            m_socket->close();
        }

        m_socket->deleteLater();
        m_socket = 0;
    }

    // Stop timer
    if (m_timer != 0)
    {
        m_timer->stop();
        m_timer->deleteLater();
        m_timer = 0;
    }
}

quint8 DataBus::Client::getClientId() const
{
    return m_clientId;
}

quint8 DataBus::Client::getActivityTimeout() const
{
    return (quint8)(m_activityTimeout / 1000);
}

quint8 DataBus::Client::getPingResponseTimeout() const
{
    return (quint8)(m_pingResponseTimeout / 1000);
}

quint8 DataBus::Client::getPingRetryCount() const
{
    return m_pingRetryCount;
}

bool DataBus::Client::registerClient(const quint8 clientId,
                                     const quint8 activityTimeout,
                                     const quint8 pingResponseTimeout,
                                     const quint8 pingRetryCount)
{
    // Check input parameters
    if ((clientId == 0) ||
        (activityTimeout < 10) ||
        (pingResponseTimeout < 10) ||
        (pingResponseTimeout > 60) ||
        (pingResponseTimeout > activityTimeout))
    {
        // Error, invalid input parameters
        return false;
    }

    // Check if already registered
    if (isRegistered())
    {
        // Error, already registered
        return false;
    }

    // Register client
    m_clientId            = clientId;
    m_activityTimeout     = activityTimeout * 1000;
    m_pingResponseTimeout = pingResponseTimeout * 1000;
    m_pingRetryCount      = pingRetryCount;

    // Activate client
    m_status = Status_Active;
    m_activityTimeoutTimer.start();

    // Start timer
    m_timer->start(1000);

    // Success
    return true;
}

bool DataBus::Client::isRegistered() const
{
    if (m_status == Status_Unregisterd)
    {
        // Not registered
        return false;
    }
    else
    {
        // Registered
        return true;
    }
}

bool DataBus::Client::isActive() const
{
    switch (m_status)
    {
        case Status_Active:
        case Status_WaitForPingResponse:
        {
            // Client is active
            return true;
        }

        default:
        {
            // Client is not active
            return false;
        }
    }
}

bool DataBus::Client::packetsAvailable() const
{
    if (m_packetList.isEmpty() == false)
    {
        return true;
    }
    else
    {
        return false;
    }
}

DataBus::Packet DataBus::Client::takePacket()
{
    if (m_packetList.empty())
    {
        return DataBus::Packet();
    }

    return m_packetList.takeFirst();
}

bool DataBus::Client::sendPacket(const Packet &packet) const
{
    // Check socket
    if (m_socket == 0)
    {
        // Error, socket is null
        return false;
    }

    // Write packet data to socket
    QByteArray packetData = packet.toBinary();

    if (m_socket->write(packetData) != packetData.size())
    {
        // Error, failed to send the packet data
        return false;
    }

    // Success
    return true;
}

quint8 DataBus::Client::getNextPacketId()
{
    return m_packetIdCounter++;
}

void DataBus::Client::processReceivedPackets()
{
    bool newPacketReceivedFlag = false;

    // Check if a packet is available
    while (m_packetParser.isPacketAvailable())
    {
        // Process received packet
        DataBus::Packet packet = m_packetParser.takePacket();

        switch (packet.getPacketType())
        {
            case DataBus::PacketType_PingResponse:
            {
                // Check if ping response is expected
                if (m_status != Status_WaitForPingResponse)
                {
                    // Error, ping response is not expected
                    break;
                }

                // Check source and destination
                if ((packet.getSource() != m_clientId) ||
                    (packet.getDestination() != 0))
                {
                    // Error, invalid source or destination
                    break;
                }

                // Check if packet ID matches to the last sent ping packet
                if (packet.getPacketId() != m_lastPingRequestPacketId)
                {
                    // Error, invalid Packet ID
                    break;
                }

                // Restart activity timeout timer and change status back to active
                m_activityTimeoutTimer.start();
                m_status = Status_Active;
                break;
            }

            default:
            {
                // Forward this to the DataBusServer
                m_packetList.append(packet);
                newPacketReceivedFlag = true;
                break;
            }
        }
    }

    if (newPacketReceivedFlag)
    {
        // At least one packet was received
        emit newPacketReceived(this);
    }
}

bool DataBus::Client::sendPingRequest()
{
    // Create Ping Request
    Packet packet;
    quint8 packetId = getNextPacketId();

    if (PingRequestPacket::create(m_clientId,
                                  packetId,
                                  &packet) == false)
    {
        // Error, failed to create packet
        return false;
    }

    // Send Ping Request
    if (sendPacket(packet) == false)
    {
        // Error, failed to send packet
        return false;
    }

    // Save Ping Request Packet ID
    m_lastPingRequestPacketId = packetId;

    // Success
    return true;
}

void DataBus::Client::disconnected()
{
    emit clientDisconnected(this);
}

void DataBus::Client::readData()
{
    // Check socket
    if (m_socket == 0)
    {
        return;
    }
    else if (m_socket->bytesAvailable() == 0)
    {
        return;
    }

    // Read available data and process it
    m_packetParser.processReceivedData(m_socket->readAll());

    // Check if new packet is available
    if (m_packetParser.isPacketAvailable() == false)
    {
        return;
    }

    // Process received packets
    processReceivedPackets();
}

void DataBus::Client::processActivity()
{
    // Check status
    switch (m_status)
    {
        case Status_Active:
        {
            if (m_activityTimeoutTimer.elapsed() >= m_activityTimeout)
            {
                // Send Ping Request
                if (sendPingRequest() == false)
                {
                    // Error, failed to send ping request
                    return;
                }

                // Wait for ping response
                m_pingResponseTimeoutTimer.start();
                m_status = Status_WaitForPingResponse;
                return;
            }

            // Wait
            return;
        }

        case Status_WaitForPingResponse:
        {
            if (m_pingResponseTimeoutTimer.elapsed() >= m_pingResponseTimeout)
            {
                // Retry ping
                if (m_pingRetryCountCounter < m_pingRetryCount)
                {
                    // Increment retry counter
                    m_pingRetryCountCounter++;

                    // Retry Ping
                    if (sendPingRequest() == false)
                    {
                        // Error, failed to send ping request
                        return;
                    }

                    // Wait for ping response
                    m_pingResponseTimeoutTimer.start();
                    return;
                }
                else
                {
                    // Failed to get a response from client: deactivate it and disconnect from the client
                    m_status = Status_Inactive;

                    if (m_socket != 0)
                    {
                        if (m_socket->isOpen())
                        {
                            m_socket->close();
                        }
                    }

                    return;
                }
            }

            // Wait
            return;
        }

        default:
        {
            // Do nothing
            return;
        }
    }
}
